/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.gateway.sync.data.nacos.handler;

import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.gateway.common.constant.NacosPathConstants;
import com.gateway.common.dto.sync.ApiSyncData;
import com.gateway.common.dto.sync.RouteFilterSyncData;
import com.gateway.common.dto.sync.RoutePredicateSyncData;
import com.gateway.common.dto.sync.RouteSyncData;
import com.gateway.common.dto.sync.PluginSyncData;
import com.gateway.common.dto.sync.RuleSyncData;
import com.gateway.common.dto.sync.SelectorSyncData;
import com.gateway.common.enums.DataEventTypeEnum;
import com.gateway.common.utils.JsonUtils;
import com.gateway.sync.data.api.ApiDataSubscriber;
import com.gateway.sync.data.api.PluginDataSubscriber;
import com.gateway.sync.data.api.RouteDataSubscriber;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * Nacos cache handler.
 */
public class NacosCacheHandler {

    protected static final Map<String, List<Listener>> LISTENERS = Maps.newConcurrentMap();

    /**
     * logger.
     */
    private static final Logger log = LoggerFactory.getLogger(NacosCacheHandler.class);

    private final ConfigService configService;

    private final PluginDataSubscriber pluginDataSubscriber;

    private final List<ApiDataSubscriber> apiDataSubscribers;

    private final List<RouteDataSubscriber> routeDataSubscribers;

    public NacosCacheHandler(final ConfigService configService, final PluginDataSubscriber pluginDataSubscriber,
                             final List<ApiDataSubscriber> apiDataSubscribers,
                             final List<RouteDataSubscriber> routeDataSubscribers) {
        this.configService = configService;
        this.pluginDataSubscriber = pluginDataSubscriber;
        this.apiDataSubscribers = apiDataSubscribers;
        this.routeDataSubscribers = routeDataSubscribers;
    }

    /**
     * get configService.
     *
     * @return configService
     */
    public ConfigService getConfigService() {
        return this.configService;
    }

    protected void updatePluginMap(final String configInfo) {
        try {
            if (StringUtils.equals(configInfo, "{}")) {
                return;
            }
            PluginSyncData pluginSyncData = JsonUtils.stringToBean(configInfo, PluginSyncData.class);
            if (Objects.isNull(pluginSyncData)) {
                log.error("sync plugin data have error: plugin sync data empty");
                return;
            }
            DataEventTypeEnum eventType = pluginSyncData.getEventType();
            if (DataEventTypeEnum.DELETE == eventType) {
                pluginDataSubscriber.unSubscribe(pluginSyncData);
            } else {
                pluginDataSubscriber.onSubscribe(pluginSyncData);
            }
        } catch (Exception e) {
            log.error("sync plugin data have error:", e);
        }
    }

    protected void updateApiDataMap(final String configInfo) {
        try {
            if (StringUtils.equals(configInfo, "{}")) {
                return;
            }
            ApiSyncData apiSyncData = JsonUtils.stringToBean(configInfo, ApiSyncData.class);
            if (Objects.isNull(apiSyncData)) {
                log.error("sync api data have error: api sync data empty");
                return;
            }
            DataEventTypeEnum eventType = apiSyncData.getEventType();
            apiDataSubscribers.forEach(subscriber -> {
                if (DataEventTypeEnum.DELETE == eventType) {
                    subscriber.unSubscribe(apiSyncData);
                } else {
                    subscriber.onSubscribe(apiSyncData);
                }
            });
        } catch (Exception e) {
            log.error("sync api data have error:", e);
        }
    }

    protected void updateRouteMap(final String configInfo) {
        try {
            if (StringUtils.equals(configInfo, "{}")) {
                return;
            }
            RouteSyncData routeSyncData = JsonUtils.stringToBean(configInfo, RouteSyncData.class);
            if (Objects.isNull(routeSyncData)) {
                log.error("sync route data have error: route sync data empty");
                return;
            }
            DataEventTypeEnum eventType = routeSyncData.getEventType();
            routeDataSubscribers.forEach(subscriber -> {
                if (DataEventTypeEnum.DELETE == eventType) {
                    subscriber.unSubscribe(routeSyncData);
                } else {
                    subscriber.onSubscribe(routeSyncData);
                }
            });
        } catch (Exception e) {
            log.error("sync route data have error:", e);
        }
    }

    public void updateRouteFilterMap(final String configInfo) {
        try {
            if (StringUtils.equals(configInfo, "{}")) {
                return;
            }
            RouteFilterSyncData routeFilterSyncData = JsonUtils.stringToBean(configInfo, RouteFilterSyncData.class);
            if (Objects.isNull(routeFilterSyncData)) {
                log.error("sync route filter data have error: route sync data empty");
                return;
            }
            DataEventTypeEnum eventType = routeFilterSyncData.getEventType();
            routeDataSubscribers.forEach(subscriber -> {
                if (DataEventTypeEnum.DELETE == eventType) {
                    subscriber.unSubscribeFilter(routeFilterSyncData);
                } else {
                    subscriber.onSubscribeFilter(routeFilterSyncData);
                }
            });
        } catch (Exception e) {
            log.error("sync route filter data have error:", e);
        }
    }

    public void updateRoutePredicateMap(final String configInfo) {
        try {
            if (StringUtils.equals(configInfo, "{}")) {
                return;
            }
            RoutePredicateSyncData predicateSyncData = JsonUtils.stringToBean(configInfo, RoutePredicateSyncData.class);
            if (Objects.isNull(predicateSyncData)) {
                log.error("sync route predicate data have error: route sync data empty");
                return;
            }
            DataEventTypeEnum eventType = predicateSyncData.getEventType();
            routeDataSubscribers.forEach(subscriber -> {
                if (DataEventTypeEnum.DELETE == eventType) {
                    subscriber.unSubscribePredicate(predicateSyncData);
                } else {
                    subscriber.onSubscribePredicate(predicateSyncData);
                }
            });
        } catch (Exception e) {
            log.error("sync route predicate data have error:", e);
        }
    }

    protected void updateSelectorMap(final String configInfo) {
        try {
            if (StringUtils.equals(configInfo, "{}")) {
                return;
            }
            SelectorSyncData selectorSyncData = JsonUtils.stringToBean(configInfo, SelectorSyncData.class);
            if (Objects.isNull(selectorSyncData)) {
                log.error("sync selector data have error: selector sync data empty");
                return;
            }
            DataEventTypeEnum eventType = selectorSyncData.getEventType();
            if (DataEventTypeEnum.DELETE == eventType) {
                pluginDataSubscriber.unSelectorSubscribe(selectorSyncData);
            } else {
                pluginDataSubscriber.onSelectorSubscribe(selectorSyncData);
            }
        } catch (Exception e) {
            log.error("sync selector data have error:", e);
        }
    }

    protected void updateRuleMap(final String configInfo) {
        try {
            if (StringUtils.equals(configInfo, "{}")) {
                return;
            }
            RuleSyncData ruleSyncData = JsonUtils.stringToBean(configInfo, RuleSyncData.class);
            if (Objects.isNull(ruleSyncData)) {
                log.error("sync rule data have error: route sync data empty");
                return;
            }
            DataEventTypeEnum eventType = ruleSyncData.getEventType();
            if (DataEventTypeEnum.DELETE == eventType) {
                pluginDataSubscriber.unRuleSubscribe(ruleSyncData);
            } else {
                pluginDataSubscriber.onRuleSubscribe(ruleSyncData);
            }
        } catch (Exception e) {
            log.error("sync rule data have error:", e);
        }
    }

    private String getConfigAndSignListener(final String dataId, final Listener listener) {
        String config = null;
        try {
            config = configService.getConfigAndSignListener(dataId, NacosPathConstants.GROUP, 6000, listener);
        } catch (NacosException e) {
            log.error(e.getMessage(), e);
        }
        if (Objects.isNull(config)) {
            config = "{}";
        }
        return config;
    }

    protected void watcherData(final String dataId, final OnChange oc) {
        Listener listener = new Listener() {
            @Override
            public void receiveConfigInfo(final String configInfo) {
                oc.change(configInfo);
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        };
        oc.change(getConfigAndSignListener(dataId, listener));
        LISTENERS.computeIfAbsent(dataId, key -> new ArrayList<>()).add(listener);
    }

    protected interface OnChange {
        void change(String changeData);
    }
}
